<!-- directive:breadcrumb ES6学习笔记 -->
<!-- directive:title 如何在浏览器和 Node 之中加载 ES6 模块 -->
<div class='panel-body'>
    <p>浏览器加载</p>
    <p>传统方法</p>
    <p>HTML 网页中，浏览器通过script标签加载 JavaScript 脚本。</p>
    <p><!-- 页面内嵌的脚本 --></p>
    <p>[script type="application/javascript"]</p>
    <p>    // module code</p>
    <p>[/script]</p>
    <p><!-- 外部脚本 --></p>
    <p>[script type="application/javascript" src="path/to/myModule.js"]</p>
    <p>[/script]</p>
    <p>上面代码中，由于浏览器脚本的默认语言是 JavaScript，因此type="application/javascript"可以省略。</p>
    <p>默认情况下，浏览器是同步加载 JavaScript 脚本，即渲染引擎遇到[script]标签就会停下来，等到执行完脚本，再继续向下渲染。如果是外部脚本，还必须加入脚本下载的时间。</p>
    <p>如果脚本体积很大，下载和执行的时间就会很长，因此造成浏览器堵塞，用户会感觉到浏览器“卡死”了，没有任何响应。这显然是很不好的体验，所以浏览器允许脚本异步加载，下面就是两种异步加载的语法。</p>
    <p>[script src="path/to/myModule.js" defer][/script]</p>
    <p>[script src="path/to/myModule.js" async][/script]</p>
    <p>上面代码中，[script]标签打开defer或async属性，脚本就会异步加载。渲染引擎遇到这一行命令，就会开始下载外部脚本，但不会等它下载和执行，而是直接执行后面的命令。</p>
    <p>defer与async的区别是：defer要等到整个页面在内存中正常渲染结束（DOM 结构完全生成，以及其他脚本执行完成），才会执行；async一旦下载完，渲染引擎就会中断渲染，执行这个脚本以后，再继续渲染。一句话，defer是“渲染完再执行”，async是“下载完就执行”。另外，如果有多个defer脚本，会按照它们在页面出现的顺序加载，而多个async脚本是不能保证加载顺序的。</p>
    <p>加载规则</p>
    <p>浏览器加载 ES6 模块，也使用[script]标签，但是要加入type="module"属性。</p>
    <p>[script type="module" src="./foo.js">[/script]</p>
    <p>上面代码在网页中插入一个模块foo.js，由于type属性设为module，所以浏览器知道这是一个 ES6 模块。</p>
    <p>浏览器对于带有type="module"的[script]，都是异步加载，不会造成堵塞浏览器，即等到整个页面渲染完，再执行模块脚本，等同于打开了[script]标签的defer属性。</p>
    <p>[script type="module" src="./foo.js">[/script]</p>
    <p><!-- 等同于 --></p>
    <p>[script type="module" src="./foo.js" defer>[/script]</p>
    <p>如果网页有多个[script type="module">，它们会按照在页面出现的顺序依次执行。</p>
    <p>[script]标签的async属性也可以打开，这时只要加载完成，渲染引擎就会中断渲染立即执行。执行完成后，再恢复渲染。</p>
    <p>[script type="module" src="./foo.js" async>[/script]</p>
    <p>一旦使用了async属性，[script type="module">就不会按照在页面出现的顺序执行，而是只要该模块加载完成，就执行该模块。</p>
    <p>ES6 模块也允许内嵌在网页中，语法行为与加载外部脚本完全一致。</p>
    <p>[script type="module"></p>
    <p>    import utils from "./utils.js";</p>
    <p>    // other code</p>
    <p>[/script]</p>
    <p>对于外部的模块脚本（上例是foo.js），有几点需要注意。</p>
    <p>代码是在模块作用域之中运行，而不是在全局作用域运行。模块内部的顶层变量，外部不可见。</p>
    <p>模块脚本自动采用严格模式，不管有没有声明use strict。</p>
    <p>模块之中，可以使用import命令加载其他模块（.js后缀不可省略，需要提供绝对 URL 或相对 URL），也可以使用export命令输出对外接口。</p>
    <p>模块之中，顶层的this关键字返回undefined，而不是指向window。也就是说，在模块顶层使用this关键字，是无意义的。</p>
    <p>同一个模块如果加载多次，将只执行一次。</p>
    <p>下面是一个示例模块。</p>
    <p>import utils from 'https://example.com/js/utils.js';</p>
    <p>const x = 1;</p>
    <p>console.log(x === window.x); //false</p>
    <p>console.log(this === undefined); // true</p>
    <p>利用顶层的this等于undefined这个语法点，可以侦测当前代码是否在 ES6 模块之中。</p>
    <p>const isNotModuleScript = this !== undefined;</p>
    <p>ES6 模块与 CommonJS 模块的差异</p>
    <p>讨论 Node 加载 ES6 模块之前，必须了解 ES6 模块与 CommonJS 模块完全不同。</p>
    <p>它们有两个重大差异。</p>
    <p>CommonJS 模块输出的是一个值的拷贝，ES6 模块输出的是值的引用。</p>
    <p>CommonJS 模块是运行时加载，ES6 模块是编译时输出接口。</p>
    <p>第二个差异是因为 CommonJS 加载的是一个对象（即module.exports属性），该对象只有在脚本运行完才会生成。而 ES6 模块不是对象，它的对外接口只是一种静态定义，在代码静态解析阶段就会生成。</p>
    <p>下面重点解释第一个差异。</p>
    <p>CommonJS 模块输出的是值的拷贝，也就是说，一旦输出一个值，模块内部的变化就影响不到这个值。请看下面这个模块文件lib.js的例子。</p>
    <p>// lib.js</p>
    <p>var counter = 3;</p>
    <p>function incCounter() {</p>
    <p>    counter++;</p>
    <p>}</p>
    <p>module.exports = {</p>
    <p>    counter: counter,</p>
    <p>    incCounter: incCounter,</p>
    <p>};</p>
    <p>上面代码输出内部变量counter和改写这个变量的内部方法incCounter。然后，在main.js里面加载这个模块。</p>
    <p>// main.js</p>
    <p>var mod = require('./lib');</p>
    <p>console.log(mod.counter);    // 3</p>
    <p>mod.incCounter();</p>
    <p>console.log(mod.counter); // 3</p>
    <p>上面代码说明，lib.js模块加载以后，它的内部变化就影响不到输出的mod.counter了。这是因为mod.counter是一个原始类型的值，会被缓存。除非写成一个函数，才能得到内部变动后的值。</p>
    <p>// lib.js</p>
    <p>var counter = 3;</p>
    <p>function incCounter() {</p>
    <p>    counter++;</p>
    <p>}</p>
    <p>module.exports = {</p>
    <p>    get counter() {</p>
    <p>        return counter</p>
    <p>    },</p>
    <p>    incCounter: incCounter,</p>
    <p>};</p>
    <p>上面代码中，输出的counter属性实际上是一个取值器函数。现在再执行main.js，就可以正确读取内部变量counter的变动了。</p>
    <p>$ node main.js</p>
    <p>3</p>
    <p>4</p>
    <p>ES6 模块的运行机制与 CommonJS 不一样。JS 引擎对脚本静态分析的时候，遇到模块加载命令import，就会生成一个只读引用。等到脚本真正执行时，再根据这个只读引用，到被加载的那个模块里面去取值。换句话说，ES6 的import有点像 Unix 系统的“符号连接”，原始值变了，import加载的值也会跟着变。因此，ES6 模块是动态引用，并且不会缓存值，模块里面的变量绑定其所在的模块。</p>
    <p>还是举上面的例子。</p>
    <p>// lib.js</p>
    <p>export let counter = 3;</p>
    <p>export function incCounter() {</p>
    <p>    counter++;</p>
    <p>}</p>
    <p>// main.js</p>
    <p>import { counter, incCounter } from './lib';</p>
    <p>console.log(counter); // 3</p>
    <p>incCounter();</p>
    <p>console.log(counter); // 4</p>
    <p>上面代码说明，ES6 模块输入的变量counter是活的，完全反应其所在模块lib.js内部的变化。</p>
    <p>再举一个出现在export一节中的例子。</p>
    <p>// m1.js</p>
    <p>export var foo = 'bar';</p>
    <p>setTimeout(() => foo = 'baz', 500);</p>
    <p>// m2.js</p>
    <p>import {foo} from './m1.js';</p>
    <p>console.log(foo);</p>
    <p>setTimeout(() => console.log(foo), 500);</p>
    <p>上面代码中，m1.js的变量foo，在刚加载时等于bar，过了 500 毫秒，又变为等于baz。</p>
    <p>让我们看看，m2.js能否正确读取这个变化。</p>
    <p>$ babel-node m2.js</p>
    <p>bar</p>
    <p>baz</p>
    <p>上面代码表明，ES6 模块不会缓存运行结果，而是动态地去被加载的模块取值，并且变量总是绑定其所在的模块。</p>
    <p>由于 ES6 输入的模块变量，只是一个“符号连接”，所以这个变量是只读的，对它进行重新赋值会报错。</p>
    <p>// lib.js</p>
    <p>export let obj = {};</p>
    <p>// main.js</p>
    <p>import { obj } from './lib';</p>
    <p>obj.prop = 123; // OK</p>
    <p>obj = {}; // TypeError</p>
    <p>上面代码中，main.js从lib.js输入变量obj，可以对obj添加属性，但是重新赋值就会报错。因为变量obj指向的地址是只读的，不能重新赋值，这就好比main.js创造了一个名为obj的const变量。</p>
    <p>最后，export通过接口，输出的是同一个值。不同的脚本加载这个接口，得到的都是同样的实例。</p>
    <p>// mod.js</p>
    <p>function C() {</p>
    <p>    this.sum = 0;</p>
    <p>    this.add = function () {</p>
    <p>        this.sum += 1;</p>
    <p>    };</p>
    <p>    this.show = function () {</p>
    <p>        console.log(this.sum);</p>
    <p>    };</p>
    <p>}</p>
    <p>export let c = new C();</p>
    <p>上面的脚本mod.js，输出的是一个C的实例。不同的脚本加载这个模块，得到的都是同一个实例。</p>
    <p>// x.js</p>
    <p>import {c} from './mod';</p>
    <p>c.add();</p>
    <p>// y.js</p>
    <p>import {c} from './mod';</p>
    <p>c.show();</p>
    <p>// main.js</p>
    <p>import './x';</p>
    <p>import './y';</p>
    <p>现在执行main.js，输出的是1。</p>
    <p>$ babel-node main.js</p>
    <p>1</p>
    <p>这就证明了x.js和y.js加载的都是C的同一个实例。</p>
    <p>Node 加载</p>
    <p>概述</p>
    <p>Node 对 ES6 模块的处理比较麻烦，因为它有自己的 CommonJS 模块格式，与 ES6 模块格式是不兼容的。目前的解决方案是，将两者分开，ES6 模块和 CommonJS 采用各自的加载方案。</p>
    <p>Node 要求 ES6 模块采用.mjs后缀文件名。也就是说，只要脚本文件里面使用import或者export命令，那么就必须采用.mjs后缀名。require命令不能加载.mjs文件，会报错，只有import命令才可以加载.mjs文件。反过来，.mjs文件里面也不能使用require命令，必须使用import。</p>
    <p>目前，这项功能还在试验阶段。安装 Node v8.5.0 或以上版本，要用--experimental-modules参数才能打开该功能。</p>
    <p>$ node --experimental-modules my-app.mjs</p>
    <p>为了与浏览器的import加载规则相同，Node 的.mjs文件支持 URL 路径。</p>
    <p>import './foo?query=1'; // 加载 ./foo 传入参数 ?query=1</p>
    <p>上面代码中，脚本路径带有参数?query=1，Node 会按 URL 规则解读。同一个脚本只要参数不同，就会被加载多次，并且保存成不同的缓存。由于这个原因，只要文件名中含有:、%、#、?等特殊字符，最好对这些字符进行转义。</p>
    <p>目前，Node 的import命令只支持加载本地模块（file:协议），不支持加载远程模块。</p>
    <p>如果模块名不含路径，那么import命令会去node_modules目录寻找这个模块。</p>
    <p>import 'baz';</p>
    <p>import 'abc/123';</p>
    <p>如果模块名包含路径，那么import命令会按照路径去寻找这个名字的脚本文件。</p>
    <p>import 'file:///etc/config/app.json';</p>
    <p>import './foo';</p>
    <p>import './foo?search';</p>
    <p>import '../bar';</p>
    <p>import '/baz';</p>
    <p>如果脚本文件省略了后缀名，比如import './foo'，Node 会依次尝试四个后缀名：./foo.mjs、./foo.js、./foo.json、./foo.node。如果这些脚本文件都不存在，Node 就会去加载./foo/package.json的main字段指定的脚本。如果./foo/package.json不存在或者没有main字段，那么就会依次加载./foo/index.mjs、./foo/index.js、./foo/index.json、./foo/index.node。如果以上四个文件还是都不存在，就会抛出错误。</p>
    <p>最后，Node 的import命令是异步加载，这一点与浏览器的处理方法相同。</p>
    <p>内部变量</p>
    <p>ES6 模块应该是通用的，同一个模块不用修改，就可以用在浏览器环境和服务器环境。为了达到这个目标，Node 规定 ES6 模块之中不能使用 CommonJS 模块的特有的一些内部变量。</p>
    <p>首先，就是this关键字。ES6 模块之中，顶层的this指向undefined；CommonJS 模块的顶层this指向当前模块，这是两者的一个重大差异。</p>
    <p>其次，以下这些顶层变量在 ES6 模块之中都是不存在的。</p>
    <p>arguments</p>
    <p>require</p>
    <p>module</p>
    <p>exports</p>
    <p>__filename</p>
    <p>__dirname</p>
    <p>如果你一定要使用这些变量，有一个变通方法，就是写一个 CommonJS 模块输出这些变量，然后再用 ES6 模块加载这个 CommonJS 模块。但是这样一来，该 ES6 模块就不能直接用于浏览器环境了，所以不推荐这样做。</p>
    <p>// expose.js</p>
    <p>module.exports = {__dirname};</p>
    <p>// use.mjs</p>
    <p>import expose from './expose.js';</p>
    <p>const {__dirname} = expose;</p>
    <p>上面代码中，expose.js是一个 CommonJS 模块，输出变量__dirname，该变量在 ES6 模块之中不存在。ES6 模块加载expose.js，就可以得到__dirname。</p>
    <p>ES6 模块加载 CommonJS 模块</p>
    <p>CommonJS 模块的输出都定义在module.exports这个属性上面。Node 的import命令加载 CommonJS 模块，Node 会自动将module.exports属性，当作模块的默认输出，即等同于export default xxx。</p>
    <p>下面是一个 CommonJS 模块。</p>
    <p>// a.js</p>
    <p>module.exports = {</p>
    <p>    foo: 'hello',</p>
    <p>    bar: 'world'</p>
    <p>};</p>
    <p>// 等同于</p>
    <p>export default {</p>
    <p>    foo: 'hello',</p>
    <p>    bar: 'world'</p>
    <p>};</p>
    <p>import命令加载上面的模块，module.exports会被视为默认输出，即import命令实际上输入的是这样一个对象{ default: module.exports }。</p>
    <p>所以，一共有三种写法，可以拿到 CommonJS 模块的module.exports。</p>
    <p>// 写法一</p>
    <p>import baz from './a';</p>
    <p>// baz = {foo: 'hello', bar: 'world'};</p>
    <p>// 写法二</p>
    <p>import {default as baz} from './a';</p>
    <p>// baz = {foo: 'hello', bar: 'world'};</p>
    <p>// 写法三</p>
    <p>import * as baz from './a';</p>
    <p>// baz = {</p>
    <p>//     get default() {return module.exports;},</p>
    <p>//     get foo() {return this.default.foo}.bind(baz),</p>
    <p>//     get bar() {return this.default.bar}.bind(baz)</p>
    <p>// }</p>
    <p>上面代码的第三种写法，可以通过baz.default拿到module.exports。foo属性和bar属性就是可以通过这种方法拿到了module.exports。</p>
    <p>下面是一些例子。</p>
    <p>// b.js</p>
    <p>module.exports = null;</p>
    <p>// es.js</p>
    <p>import foo from './b';</p>
    <p>// foo = null;</p>
    <p>import * as bar from './b';</p>
    <p>// bar = { default:null };</p>
    <p>上面代码中，es.js采用第二种写法时，要通过bar.default这样的写法，才能拿到module.exports。</p>
    <p>// c.js</p>
    <p>module.exports = function two() {</p>
    <p>    return 2;</p>
    <p>};</p>
    <p>// es.js</p>
    <p>import foo from './c';</p>
    <p>foo(); // 2</p>
    <p>import * as bar from './c';</p>
    <p>bar.default(); // 2</p>
    <p>bar(); // throws, bar is not a function</p>
    <p>上面代码中，bar本身是一个对象，不能当作函数调用，只能通过bar.default调用。</p>
    <p>CommonJS 模块的输出缓存机制，在 ES6 加载方式下依然有效。</p>
    <p>// foo.js</p>
    <p>module.exports = 123;</p>
    <p>setTimeout(_ => module.exports = null);</p>
    <p>上面代码中，对于加载foo.js的脚本，module.exports将一直是123，而不会变成null。</p>
    <p>由于 ES6 模块是编译时确定输出接口，CommonJS 模块是运行时确定输出接口，所以采用import命令加载 CommonJS 模块时，不允许采用下面的写法。</p>
    <p>// 不正确</p>
    <p>import { readFile } from 'fs';</p>
    <p>上面的写法不正确，因为fs是 CommonJS 格式，只有在运行时才能确定readFile接口，而import命令要求编译时就确定这个接口。解决方法就是改为整体输入。</p>
    <p>// 正确的写法一</p>
    <p>import * as express from 'express';</p>
    <p>const app = express.default();</p>
    <p>// 正确的写法二</p>
    <p>import express from 'express';</p>
    <p>const app = express();</p>
    <p>CommonJS 模块加载 ES6 模块</p>
    <p>CommonJS 模块加载 ES6 模块，不能使用require命令，而要使用import()函数。ES6 模块的所有输出接口，会成为输入对象的属性。</p>
    <p>// es.mjs</p>
    <p>let foo = { bar: 'my-default' };</p>
    <p>export default foo;</p>
    <p>// cjs.js</p>
    <p>const es_namespace = await import('./es.mjs');</p>
    <p>// es_namespace = {</p>
    <p>//     get default() {</p>
    <p>//         ...</p>
    <p>//     }</p>
    <p>// }</p>
    <p>console.log(es_namespace.default);</p>
    <p>// { bar:'my-default' }</p>
    <p>上面代码中，default接口变成了es_namespace.default属性。</p>
    <p>下面是另一个例子。</p>
    <p>// es.js</p>
    <p>export let foo = { bar:'my-default' };</p>
    <p>export { foo as bar };</p>
    <p>export function f() {};</p>
    <p>export class c {};</p>
    <p>// cjs.js</p>
    <p>const es_namespace = await import('./es');</p>
    <p>// es_namespace = {</p>
    <p>//     get foo() {return foo;}</p>
    <p>//     get bar() {return foo;}</p>
    <p>//     get f() {return f;}</p>
    <p>//     get c() {return c;}</p>
    <p>// }</p>
    <p>循环加载</p>
    <p>“循环加载”（circular dependency）指的是，a脚本的执行依赖b脚本，而b脚本的执行又依赖a脚本。</p>
    <p>// a.js</p>
    <p>var b = require('b');</p>
    <p>// b.js</p>
    <p>var a = require('a');</p>
    <p>通常，“循环加载”表示存在强耦合，如果处理不好，还可能导致递归加载，使得程序无法执行，因此应该避免出现。</p>
    <p>但是实际上，这是很难避免的，尤其是依赖关系复杂的大项目，很容易出现a依赖b，b依赖c，c又依赖a这样的情况。这意味着，模块加载机制必须考虑“循环加载”的情况。</p>
    <p>对于 JavaScript 语言来说，目前最常见的两种模块格式 CommonJS 和 ES6，处理“循环加载”的方法是不一样的，返回的结果也不一样。</p>
    <p>CommonJS 模块的加载原理</p>
    <p>介绍 ES6 如何处理“循环加载”之前，先介绍目前最流行的 CommonJS 模块格式的加载原理。</p>
    <p>CommonJS 的一个模块，就是一个脚本文件。require命令第一次加载该脚本，就会执行整个脚本，然后在内存生成一个对象。</p>
    <p>{</p>
    <p>    id: '...',</p>
    <p>    exports: { ... },</p>
    <p>    loaded: true,</p>
    <p>    ...</p>
    <p>}</p>
    <p>上面代码就是 Node 内部加载模块后生成的一个对象。该对象的id属性是模块名，exports属性是模块输出的各个接口，loaded属性是一个布尔值，表示该模块的脚本是否执行完毕。其他还有很多属性，这里都省略了。</p>
    <p>以后需要用到这个模块的时候，就会到exports属性上面取值。即使再次执行require命令，也不会再次执行该模块，而是到缓存之中取值。也就是说，CommonJS 模块无论加载多少次，都只会在第一次加载时运行一次，以后再加载，就返回第一次运行的结果，除非手动清除系统缓存。</p>
    <p>CommonJS 模块的循环加载</p>
    <p>CommonJS 模块的重要特性是加载时执行，即脚本代码在require的时候，就会全部执行。一旦出现某个模块被"循环加载"，就只输出已经执行的部分，还未执行的部分不会输出。</p>
    <p>让我们来看，Node 官方文档里面的例子。脚本文件a.js代码如下。</p>
    <p>exports.done = false;</p>
    <p>var b = require('./b.js');</p>
    <p>console.log('在 a.js 之中，b.done = %j', b.done);</p>
    <p>exports.done = true;</p>
    <p>console.log('a.js 执行完毕');</p>
    <p>上面代码之中，a.js脚本先输出一个done变量，然后加载另一个脚本文件b.js。注意，此时a.js代码就停在这里，等待b.js执行完毕，再往下执行。</p>
    <p>再看b.js的代码。</p>
    <p>exports.done = false;</p>
    <p>var a = require('./a.js');</p>
    <p>console.log('在 b.js 之中，a.done = %j', a.done);</p>
    <p>exports.done = true;</p>
    <p>console.log('b.js 执行完毕');</p>
    <p>上面代码之中，b.js执行到第二行，就会去加载a.js，这时，就发生了“循环加载”。系统会去a.js模块对应对象的exports属性取值，可是因为a.js还没有执行完，从exports属性只能取回已经执行的部分，而不是最后的值。</p>
    <p>a.js已经执行的部分，只有一行。</p>
    <p>exports.done = false;</p>
    <p>因此，对于b.js来说，它从a.js只输入一个变量done，值为false。</p>
    <p>然后，b.js接着往下执行，等到全部执行完毕，再把执行权交还给a.js。于是，a.js接着往下执行，直到执行完毕。我们写一个脚本main.js，验证这个过程。</p>
    <p>var a = require('./a.js');</p>
    <p>var b = require('./b.js');</p>
    <p>console.log('在 main.js 之中, a.done=%j, b.done=%j', a.done, b.done);</p>
    <p>执行main.js，运行结果如下。</p>
    <p>$ node main.js</p>
    <p>在 b.js 之中，a.done = false</p>
    <p>b.js 执行完毕</p>
    <p>在 a.js 之中，b.done = true</p>
    <p>a.js 执行完毕</p>
    <p>在 main.js 之中, a.done=true, b.done=true</p>
    <p>上面的代码证明了两件事。一是，在b.js之中，a.js没有执行完毕，只执行了第一行。二是，main.js执行到第二行时，不会再次执行b.js，而是输出缓存的b.js的执行结果，即它的第四行。</p>
    <p>exports.done = true;</p>
    <p>总之，CommonJS 输入的是被输出值的拷贝，不是引用。</p>
    <p>另外，由于 CommonJS 模块遇到循环加载时，返回的是当前已经执行的部分的值，而不是代码全部执行后的值，两者可能会有差异。所以，输入变量的时候，必须非常小心。</p>
    <p>var a = require('a'); // 安全的写法</p>
    <p>var foo = require('a').foo; // 危险的写法</p>
    <p>exports.good = function (arg) {</p>
    <p>    return a.foo('good', arg); // 使用的是 a.foo 的最新值</p>
    <p>};</p>
    <p>exports.bad = function (arg) {</p>
    <p>    return foo('bad', arg); // 使用的是一个部分加载时的值</p>
    <p>};</p>
    <p>上面代码中，如果发生循环加载，require('a').foo的值很可能后面会被改写，改用require('a')会更保险一点。</p>
    <p>ES6 模块的循环加载</p>
    <p>ES6 处理“循环加载”与 CommonJS 有本质的不同。ES6 模块是动态引用，如果使用import从一个模块加载变量（即import foo from 'foo'），那些变量不会被缓存，而是成为一个指向被加载模块的引用，需要开发者自己保证，真正取值的时候能够取到值。</p>
    <p>请看下面这个例子。</p>
    <p>// a.mjs</p>
    <p>import {bar} from './b';</p>
    <p>console.log('a.mjs');</p>
    <p>console.log(bar);</p>
    <p>export let foo = 'foo';</p>
    <p>// b.mjs</p>
    <p>import {foo} from './a';</p>
    <p>console.log('b.mjs');</p>
    <p>console.log(foo);</p>
    <p>export let bar = 'bar';</p>
    <p>上面代码中，a.mjs加载b.mjs，b.mjs又加载a.mjs，构成循环加载。执行a.mjs，结果如下。</p>
    <p>$ node --experimental-modules a.mjs</p>
    <p>b.mjs</p>
    <p>ReferenceError: foo is not defined</p>
    <p>上面代码中，执行a.mjs以后会报错，foo变量未定义，这是为什么？</p>
    <p>让我们一行行来看，ES6 循环加载是怎么处理的。首先，执行a.mjs以后，引擎发现它加载了b.mjs，因此会优先执行b.mjs，然后再执行a.mjs。接着，执行b.mjs的时候，已知它从a.mjs输入了foo接口，这时不会去执行a.mjs，而是认为这个接口已经存在了，继续往下执行。执行到第三行console.log(foo)的时候，才发现这个接口根本没定义，因此报错。</p>
    <p>解决这个问题的方法，就是让b.mjs运行的时候，foo已经有定义了。这可以通过将foo写成函数来解决。</p>
    <p>// a.mjs</p>
    <p>import {bar} from './b';</p>
    <p>console.log('a.mjs');</p>
    <p>console.log(bar());</p>
    <p>function foo() { return 'foo' }</p>
    <p>export {foo};</p>
    <p>// b.mjs</p>
    <p>import {foo} from './a';</p>
    <p>console.log('b.mjs');</p>
    <p>console.log(foo());</p>
    <p>function bar() { return 'bar' }</p>
    <p>export {bar};</p>
    <p>这时再执行a.mjs就可以得到预期结果。</p>
    <p>$ node --experimental-modules a.mjs</p>
    <p>b.mjs</p>
    <p>foo</p>
    <p>a.mjs</p>
    <p>bar</p>
    <p>这是因为函数具有提升作用，在执行import {bar} from './b'时，函数foo就已经有定义了，所以b.mjs加载的时候不会报错。这也意味着，如果把函数foo改写成函数表达式，也会报错。</p>
    <p>// a.mjs</p>
    <p>import {bar} from './b';</p>
    <p>console.log('a.mjs');</p>
    <p>console.log(bar());</p>
    <p>const foo = () => 'foo';</p>
    <p>export {foo};</p>
    <p>上面代码的第四行，改成了函数表达式，就不具有提升作用，执行就会报错。</p>
    <p>我们再来看 ES6 模块加载器SystemJS给出的一个例子。</p>
    <p>// even.js</p>
    <p>import { odd } from './odd'</p>
    <p>export var counter = 0;</p>
    <p>export function even(n) {</p>
    <p>    counter++;</p>
    <p>    return n === 0 || odd(n - 1);</p>
    <p>}</p>
    <p>// odd.js</p>
    <p>import { even } from './even';</p>
    <p>export function odd(n) {</p>
    <p>    return n !== 0 && even(n - 1);</p>
    <p>}</p>
    <p>上面代码中，even.js里面的函数even有一个参数n，只要不等于 0，就会减去 1，传入加载的odd()。odd.js也会做类似操作。</p>
    <p>运行上面这段代码，结果如下。</p>
    <p>$ babel-node</p>
    <p>> import * as m from './even.js';</p>
    <p>> m.even(10);</p>
    <p>true</p>
    <p>> m.counter</p>
    <p>6</p>
    <p>> m.even(20)</p>
    <p>true</p>
    <p>> m.counter</p>
    <p>17</p>
    <p>上面代码中，参数n从 10 变为 0 的过程中，even()一共会执行 6 次，所以变量counter等于 6。第二次调用even()时，参数n从 20 变为 0，even()一共会执行 11 次，加上前面的 6 次，所以变量counter等于 17。</p>
    <p>这个例子要是改写成 CommonJS，就根本无法执行，会报错。</p>
    <p>// even.js</p>
    <p>var odd = require('./odd');</p>
    <p>var counter = 0;</p>
    <p>exports.counter = counter;</p>
    <p>exports.even = function (n) {</p>
    <p>    counter++;</p>
    <p>    return n == 0 || odd(n - 1);</p>
    <p>}</p>
    <p>// odd.js</p>
    <p>var even = require('./even').even;</p>
    <p>module.exports = function (n) {</p>
    <p>    return n != 0 && even(n - 1);</p>
    <p>}</p>
    <p>上面代码中，even.js加载odd.js，而odd.js又去加载even.js，形成“循环加载”。这时，执行引擎就会输出even.js已经执行的部分（不存在任何结果），所以在odd.js之中，变量even等于undefined，等到后面调用even(n - 1)就会报错。</p>
    <p>$ node</p>
    <p>> var m = require('./even');</p>
    <p>> m.even(10)</p>
    <p>TypeError: even is not a function</p>
    <p>ES6 模块的转码</p>
    <p>浏览器目前还不支持 ES6 模块，为了现在就能使用，可以将其转为 ES5 的写法。除了 Babel 可以用来转码之外，还有以下两个方法，也可以用来转码。</p>
    <p>ES6 module transpiler</p>
    <p>ES6 module transpiler是 square 公司开源的一个转码器，可以将 ES6 模块转为 CommonJS 模块或 AMD 模块的写法，从而在浏览器中使用。</p>
    <p>首先，安装这个转码器。</p>
    <p>$ npm install -g es6-module-transpiler</p>
    <p>然后，使用compile-modules convert命令，将 ES6 模块文件转码。</p>
    <p>$ compile-modules convert file1.js file2.js</p>
    <p>-o参数可以指定转码后的文件名。</p>
    <p>$ compile-modules convert -o out.js file1.js</p>
    <p>SystemJS</p>
    <p>另一种解决方法是使用 SystemJS。它是一个垫片库（polyfill），可以在浏览器内加载 ES6 模块、AMD 模块和 CommonJS 模块，将其转为 ES5 格式。它在后台调用的是 Google 的 Traceur 转码器。</p>
    <p>使用时，先在网页内载入system.js文件。</p>
    <p>[script src="system.js">[/script]</p>
    <p>然后，使用System.import方法加载模块文件。</p>
    <p>[script]</p>
    <p>    System.import('./app.js');</p>
    <p>[/script]</p>
    <p>上面代码中的./app，指的是当前目录下的 app.js 文件。它可以是 ES6 模块文件，System.import会自动将其转码。</p>
    <p>需要注意的是，System.import使用异步加载，返回一个 Promise 对象，可以针对这个对象编程。下面是一个模块文件。</p>
    <p>// app/es6-file.js:</p>
    <p>export class q {</p>
    <p>    constructor() {</p>
    <p>        this.es6 = 'hello';</p>
    <p>    }</p>
    <p>}</p>
    <p>然后，在网页内加载这个模块文件。</p>
    <p>[script]</p>
    <p>System.import('app/es6-file').then(function(m) {</p>
    <p>    console.log(new m.q().es6); // hello</p>
    <p>});</p>
    <p>[/script]</p>
    <p>上面代码中，System.import方法返回的是一个 Promise 对象，所以可以用then方法指定回调函数。</p>
</div>
